package com.freecloud.plug.jedis.distlock;

import redis.clients.jedis.Jedis;

import java.util.Collections;

/**
 * 使用jedis客户端实现分布式锁
 *
 * 在该例子中虽然使用了lua脚本与setnx方式实现加锁与释放问题
 * 但还有一个问题，如果我们的程序执行时间比加锁时间长，会导致redis删除过期Key而我们的业务代码还未执行完，
 * 导致我们的数据一致性产生问题。
 *
 * @Author: maomao
 * @Date: 2021-04-27 08:42
 */
public class DistLock {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        // set支持多个参数 NX（not exist） XX（exist） EX（seconds） PX（million seconds）
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 直接删除解锁，未判断客户端ID，会导致其他客户端把锁释放
     * @param jedis
     * @param lockKey
     */
    public static void releaseLock1(Jedis jedis, String lockKey) {
        jedis.del(lockKey);
    }

    /**
     * 增加锁判断，但因判断与删除不是原子操作，在并发场景时，会导致错误删除
     * @param jedis
     * @param lockKey
     * @param requestId
     */
    public static void releaseLock2(Jedis jedis, String lockKey, String requestId) {
        // 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 若在此时，这把锁突然不是这个客户端的，则会误解锁
            // 两个操作不能保证原子性
            jedis.del(lockKey);
        }
    }

    /**
     * 释放分布式锁，使用lua脚本删除，可确保判断与删除的原子操作
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}
